fix(drivers): map emit keys onto the names the Nova payload reads#668
Conversation
nova.DerTelemetry unmarshals a driver's emit blob directly, so a key it does not recognise is dropped without an error and the value never leaves the gateway. Catalog drivers emit import_wh, export_wh, charge_wh, discharge_wh, lifetime_wh and hz. DerTelemetry reads total_import_wh, total_export_wh, total_charge_wh, total_discharge_wh, total_generation_wh and freq_hz. None of those six has ever reached Nova from a catalog driver. Our own zap.lua works around it by emitting both names for every affected field, which is why the gap went unnoticed. Normalise at the emit boundary instead, so every driver is fixed at once. The canonical @srcful/data-models spellings map onto the same internal names, so srcfl/device-drivers can convert its catalog without a further host change. An alias never overwrites a key the driver set under the target name, and the driver's original keys are kept because the UI and API surface a reading verbatim. A parse failure returns the input untouched rather than losing a reading. Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 41cf5f2b10
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| "export_wh": "total_export_wh", | ||
| "charge_wh": "total_charge_wh", | ||
| "discharge_wh": "total_discharge_wh", | ||
| "lifetime_wh": "total_generation_wh", |
There was a problem hiding this comment.
Restrict the lifetime-energy alias to PV telemetry
When nova.schema_mode: unified is enabled, this unconditional alias mislabels EV charger energy: ctek.lua, ctek_v2.lua, and ctek_hybrid.lua emit lifetime_wh on type:"ev" readings, but the rewrite populates DerTelemetry.TotalGenerationWh, which is a PV-only field, and unified encoding serializes it verbatim. Those EV reports therefore advertise lifetime consumed energy as PV generation; inspect the reading type before applying this alias or introduce an EV-specific destination.
Useful? React with 👍 / 👎.
Summary
nova.DerTelemetryunmarshals a driver's emit blob directly, so a key it doesnot recognise is dropped without an error and the value never leaves the
gateway.
Catalog drivers emit
import_wh,export_wh,charge_wh,discharge_wh,lifetime_whandhz.DerTelemetryreadstotal_import_wh,total_export_wh,total_charge_wh,total_discharge_wh,total_generation_whandfreq_hz.None of those six has ever reached Nova from a catalog driver. Energy
counters and grid frequency, from every driver in
srcfl/device-drivers.Our own
zap.luaworks around it per field —reading.import_wh = reading.total_import_wh— which is why the gap went unnoticed: the driverswritten here happened to do the right thing.
What changed
normalizeTelemetryKeysrewrites known aliases onto the names the payloadreads, at the emit boundary, so every driver is fixed at once.
The canonical
@srcful/data-modelsspellings map onto the same internal names,so
srcfl/device-driverscan convert its catalog toW,Hz,L1_V,SoC_nom_fractandtotal_import_Whwithout a further host change.Three rules keep it safe:
verbatim and removing one would change what an operator sees;
validated by this point, and normalisation must never lose a reading.
Tests
go/internal/drivers/telemetry_keys_test.go:hz,import_wh,export_wh) land on the Nova names;charge_wh,discharge_wh,lifetime_wh) likewise;W,Hz,L1_V,total_import_Wh) land on the samenames;
drivers are unaffected;
go test ./internal/drivers/ ./internal/nova/ ./internal/telemetry/passes.Follow-up
rated_w→rated_power_wand the batteryv/a→dc_v/dc_amappingsare deliberately left out. Both need a look at what the drivers actually mean
by those fields before mapping them, and neither is an energy counter that
silently reads as zero.
🤖 Generated with Claude Code